
/***************************************************************************
 *   Copyright 2006-2009 by Christian Ihle                                 *
 *   kontakt@usikkert.net                                                  *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

package net.usikkert.kouchat.autocomplete;

import java.util.ArrayList;
import java.util.List;

/**
 * This class can give suggestions for autocompleting words.
 *
 * At least one {@link AutoCompleteList} with some words is needed
 * to get results.
 *
 * @author Christian Ihle
 */
public class AutoCompleter
{
	/**
	 * The word that was used to build autocomplete suggestions in the
	 * previous search.
	 */
	private String lastWord;

	/**
	 * The suggested autocompleted word from previous search.
	 */
	private String lastCompletedWord;

	/**
	 * The full line of text from the previous search, with the autocompleted
	 * word replacing the original word.
	 */
	private String lastCompletedLine;

	/**
	 * The position in the {@link #lastCompletedLine} that marks the end
	 * of the {@link #lastCompletedWord}. Useful for setting the caret
	 * at the end of the autocompleted word.
	 */
	private int newCaretPosition;

	/**
	 * A list of {@link AutoCompleteList}s with support for different
	 * kinds of words to autocomplete.
	 */
	private final List<AutoCompleteList> autoCompleteLists;

	/**
	 * Constructor. Initializes variables.
	 */
	public AutoCompleter()
	{
		lastCompletedLine = "";
		lastCompletedWord = "";
		lastWord = "";
		autoCompleteLists = new ArrayList<AutoCompleteList>();
	}

	/**
	 * Extracts the word at the given position from the line,
	 * and looks for suggestions for autocompleting the word.
	 * <br><br>
	 * The previous search is saved, so if there are more than one
	 * suggestion, repeated calls to this method will give the next
	 * result in the suggestion list.
	 *
	 * @param line The line of text where the word to autocomplete is.
	 * @param caretPosition The position in the line to look for the word.
	 * @return The complete line, where the original word is replaced by
	 * 		the suggested autocompleted word.
	 * 		Use {@link #getNewCaretPosition()} to get the new caret position.
	 */
	public String completeWord( final String line, final int caretPosition )
	{
		String completedLine = "";

		if ( autoCompleteLists.size() > 0 )
		{
			final int stop = findStopPosition( line, caretPosition );
			final int start = findStartPosition( line, caretPosition );
			final String word = line.substring( start, stop );

			if ( word.trim().length() > 0 )
			{
				final boolean continueLastSearch = continueLastSearch( word, line );
				String checkword = "";

				if ( continueLastSearch )
					checkword = lastWord;
				else
					checkword = word;

				AutoCompleteList autoCompleteList = getAutoCompleteList( checkword );

				if ( autoCompleteList != null )
				{
					List<String> suggestions = getAutoCompleteSuggestions(
							autoCompleteList.getWordList(), checkword );

					if ( suggestions.size() > 0 )
					{
						int nextSuggestionPosition = findNextSuggestionPosition(
								continueLastSearch, suggestions, word );
						String newWord = suggestions.get( nextSuggestionPosition );
						completedLine = line.substring( 0, start ) + newWord;
						newCaretPosition = completedLine.length();
						completedLine += line.substring( stop );
						lastCompletedLine = completedLine;
						lastCompletedWord = newWord;

						if ( !continueLastSearch )
							lastWord = word;
					}
				}
			}
		}

		return completedLine;
	}

	/**
	 * Finds where in the list of suggestions to get the next suggestion.
	 *
	 * @param continueLastSearch If the previous search should be continued.
	 * @param suggestions The list of suggested words.
	 * @param word The word that is going to be autocompleted by the suggestion
	 * 		this method finds. If this search continues from the previous
	 * 		search, the word will be the same as the suggestion from that search.
	 * @return The position in the list where the next suggestion can be found.
	 */
	private int findNextSuggestionPosition( final boolean continueLastSearch,
			final List<String> suggestions, final String word )
	{
		int nextSuggestionPosition = -1;

		if ( continueLastSearch )
		{
			// Locate the position of the previous suggestion in the list
			for ( int i = 0; i < suggestions.size(); i++ )
			{
				if ( suggestions.get( i ).equals( word ) )
				{
					nextSuggestionPosition = i;
					break;
				}
			}

			/* If more suggestions are available, increase position,
			 * or else start from the beginning again. */
			if ( nextSuggestionPosition > -1 && nextSuggestionPosition < suggestions.size() - 1 )
				nextSuggestionPosition++;
			else
				nextSuggestionPosition = 0;
		}

		// New search, start with first suggestion
		if ( nextSuggestionPosition == -1 )
			nextSuggestionPosition = 0;

		return nextSuggestionPosition;
	}

	/**
	 * Checks if the new search should continue where the last left off.
	 * The reason to continue is to see if there are more matches to the
	 * previous search.
	 * <br><br>
	 * To find this, a check is done to see if the previous autocompleted
	 * word and line is the same as the new word and line. If that's the case,
	 * no changes to the text has been done since last autocomplete attempt.
	 *
	 * @param word The word to compare against the previous autocompleted word.
	 * @param line The line to compare against the previous autocompleted line.
	 * @return True if the search should be continued instead of restarted.
	 */
	private boolean continueLastSearch( final String word, final String line )
	{
		return lastCompletedWord.equals( word ) && lastCompletedLine.equals( line );
	}

	/**
	 * Locates the position in the line where the word ends.
	 *
	 * @param line The line of text where the word is.
	 * @param caretPosition The position in the line where the word is.
	 * @return The position where the word ends.
	 */
	private int findStopPosition( final String line, final int caretPosition )
	{
		int stop = line.indexOf( ' ', caretPosition );

		if ( stop == -1 )
			stop = line.length();

		return stop;
	}

	/**
	 * Locates the position in the line where the word starts.
	 *
	 * @param line The line of text where the word is.
	 * @param caretPosition The position in the line where the word is.
	 * @return The position where the word starts.
	 */
	private int findStartPosition( final String line, final int caretPosition )
	{
		int start = line.lastIndexOf( ' ', caretPosition - 1 );

		if ( start == -1 )
			start = 0;
		else
			start++;

		return start;
	}

	/**
	 * Asks the {@link AutoCompleteList}s available if any of them supports
	 * this kind of word, and returns the match, if found.
	 *
	 * @param word The word to ask if any {@link AutoCompleteList} supports.
	 * @return The first {@link AutoCompleteList} to support that word,
	 * 		or <em>null</em> if none.
	 */
	private AutoCompleteList getAutoCompleteList( final String word )
	{
		for ( AutoCompleteList acl : autoCompleteLists )
		{
			if ( acl.acceptsWord( word ) )
			{
				return acl;
			}
		}

		return null;
	}

	/**
	 * Compares the word with the word list to find suggestions for
	 * autocompleting the word.
	 *
	 * @param wordList A list of words to compare the word with.
	 * @param word The word to get suggestions for.
	 * @return A list of suggestions.
	 */
	private List<String> getAutoCompleteSuggestions( final String[] wordList, final String word )
	{
		List<String> suggestions = new ArrayList<String>();

		for ( int i = 0; i < wordList.length; i++ )
		{
			if ( wordList[i].toLowerCase().startsWith( word.toLowerCase() ) )
			{
				suggestions.add( wordList[i] );
			}
		}

		return suggestions;
	}

	/**
	 * Returns the new caret position for the last completed search.
	 *
	 * @return The new caret position.
	 */
	public int getNewCaretPosition()
	{
		return newCaretPosition;
	}

	/**
	 * Adds a new {@link AutoCompleteList} to use for autocompletion.
	 *
	 * @param acl The list to add.
	 */
	public void addAutoCompleteList( final AutoCompleteList acl )
	{
		autoCompleteLists.add( acl );
	}
}
